package servicectl

import (
	"context"
	"errors"
	"fmt"
	"github.com/gogf/gf/errors/gerror"
	"github.com/spf13/afero"
	"gitlab.local/golibrary/apputils"
	"gitlab.local/golibrary/utils"
	"io/fs"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"sync"
	"time"
)

type serviceSlice struct {
	list []string
	srv  *service
}

// SetStartup 设置自启动
func SetStartup(name string, ok bool) error {
	if !utils.Exists(dir) {
		if err := os.Mkdir(dir, os.ModePerm); err != nil {
			return errors.New("SetStartup Mkdir fail, err: " + err.Error())
		}
	}
	if filepath.IsAbs(name) {
		name = filepath.Base(name)
	}
	isLink, _ := utils.IsSymlink(filepath.Join(initrd, name))
	path := filepath.Join(dir, name)
	if isLink {
		path = filepath.Join(appDir, name)
	}
	if ok {
		return utils.TouchFile(path)
	}
	if utils.Exists(path) {
		return os.Remove(path)
	}
	return nil
}

// SetStartup 设置自启动
func (Self *service) SetStartup(name string, ok bool) error {
	if !utils.Exists(dir) {
		if err := os.Mkdir(dir, os.ModePerm); err != nil {
			return gerror.Wrap(err, "failed to create dir")
		}
	}
	if filepath.IsAbs(name) {
		name = filepath.Base(name)
	}
	isLink, _ := utils.IsSymlink(filepath.Join(initrd, name))
	path := filepath.Join(dir, name)
	if isLink {
		path = filepath.Join(appDir, name)
	}
	if ok {
		return utils.TouchFile(path)
	}
	if utils.Exists(path) {
		return os.Remove(path)
	}
	return nil
}

func (Self *service) Status(name string) (string, error) {
	s := name
	if !filepath.IsAbs(name) {
		s = filepath.Join(initrd, name)
	}
	return utils.ShellExec(s, "status")
}

func (Self *service) Start(name string) error {
	s := name
	if !filepath.IsAbs(name) {
		s = filepath.Join(initrd, name)
	}
	return Self.safeStart(s)
}

func (Self *service) BuildScript(startOrStop bool, name string) string {
	if startOrStop {
		return fmt.Sprintf("%s/%s start", initrd, name)
	}
	return fmt.Sprintf("%s/%s stop", initrd, name)
}

func (Self *service) ReStart(name string) error {
	s := name
	if !filepath.IsAbs(name) {
		s = filepath.Join(initrd, name)
	}
	_, err := utils.ShellExec(s, "restart")
	return err
}

func (Self *service) ReLoad(name string) error {
	s := name
	if !filepath.IsAbs(name) {
		s = filepath.Join(initrd, name)
	}
	_, err := utils.ShellExec(s, "reload")
	return err
}

func (Self *service) Stop(name string) error {
	s := name
	if !filepath.IsAbs(name) {
		s = filepath.Join(initrd, name)
	}
	return Self.safeStop(s)
}

func (Self *service) ForceStop(name string) error {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*50)
	defer cancel()
	return utils.RunCmd(exec.CommandContext(ctx, filepath.Join(initrd, name), "stop"))
}

func (Self *service) IsRunningWithName(name string) bool {
	s := name
	if !filepath.IsAbs(name) {
		s = filepath.Join(initrd, name)
	}
	f, _ := exec.Command(s, "status").Output()
	out := strings.TrimSpace(string(f))
	if s != "" && strings.Contains(out, "running") {
		return true
	}
	return false
}

// IsRunning check if the process running
func (Self *service) IsRunning(name string) bool {
	if filepath.IsAbs(name) {
		if utils.Exists(name) {
			f, _ := exec.Command(name, "status").Output()
			s := strings.TrimSpace(string(f))
			if s != "" && strings.Contains(s, "running") {
				return true
			}
			return false
		}
		name = filepath.Base(name)
	}
	b, _ := exec.Command("pidof", name).Output()
	return len(b) > 0
}

// GetAutoStartupServices list services that run on startup
func (Self *service) GetAutoStartupServices() ([]string, error) {
	result := make([]string, 0)
	files, err := Self.loadServiceNames(dir)
	if err != nil {
		return nil, gerror.Wrap(err, "failed to read dir")
	}
	result = files
	files, err = Self.loadServiceNames(appDir)
	if err != nil {
		return nil, gerror.Wrap(err, "failed to read appDir")
	}
	result = append(result, files...)
	return result, nil
}

func (Self *service) loadServiceNames(path string) ([]string, error) {
	result := make([]string, 0)
	files, err := afero.ReadDir(Self.fs, path)
	if err != nil {
		return nil, gerror.Wrap(err, "failed to read dir")
	}
	for _, file := range files {
		result = append(result, file.Name())
	}
	return result, nil
}

func (Self *service) safeStart(path string) error {
	if Self.IsRunning(path) {
		return nil
	}
	if _, err := os.Stat(path); err != nil {
		_, _ = utils.ShellExec(fmt.Sprintf("/usr/bin/post-install -id %s", filepath.Base(path)))
	}
	cmd := exec.Command(path, "start")
	err := cmd.Start()
	//避免僵尸进程
	go cmd.Wait()
	time.Sleep(2 * time.Second)
	return err
}

func (Self *service) safeStop(path string) error {
	var err error
	cmd := exec.Command(path, "stop")
	for i := 0; i < 3; i++ {
		if !Self.IsRunning(path) {
			err = nil
			break
		}
		err = cmd.Run()
		time.Sleep(1 * time.Second)
	}
	return err
}

// SysService
// event true: 表示start, false: 表示stop
func (Self *service) SysService(event bool, exclude ...string) error {
	if exclude == nil {
		exclude = []string{}
	}
	return fs.WalkDir(os.DirFS(dir), ".", func(path string, d fs.DirEntry, err error) error {
		if path == "." {
			return nil
		}
		if d.IsDir() {
			return fs.SkipDir
		}
		id := d.Name()
		if utils.Contains(exclude, id) > -1 {
			return nil
		}
		script := filepath.Join(initrd, id)
		isLink, err := utils.IsSymlink(script)
		if err != nil || isLink {
			return nil
		}
		if event {
			fmt.Print(script, "\tstart")
			_ = Self.safeStart(script)
			fmt.Println("", "done")
		} else {
			fmt.Print(script, "\tstop")
			_ = Self.safeStop(script)
			fmt.Println("", "done")
		}
		return nil
	})
}

func (Self *service) AppServiceByVolume(volume string, event bool, exclude ...string) error {
	if exclude == nil {
		exclude = []string{}
	}
	list, err := os.ReadDir(appDir)
	if err != nil {
		return err
	}
	appMap := make(map[string]serviceSlice)
	for _, item := range list {
		info := apputils.CheckAppState(item.Name())
		if info == nil || !strings.HasPrefix(info.InstallPath, volume) {
			continue
		}
		appMap[info.ID] = serviceSlice{
			list: info.Depend,
			srv:  Self,
		}
	}
	for id, child := range appMap {
		depends := make([]string, 0)
		if len(child.list) > 0 {
			for _, cid := range child.list {
				if dd, ok := appMap[cid]; ok {
					depends = append(depends, dd.list...)
					delete(appMap, cid)
				}
				depends = append(depends, cid)
			}
		}
		depends = append(depends, id)
		appMap[id] = serviceSlice{
			list: utils.RemoveDup(depends),
			srv:  Self,
		}
	}
	wg := sync.WaitGroup{}
	for _, item := range appMap {
		wg.Add(1)
		go func(chs serviceSlice, event bool) {
			chs.sortService(event, exclude)
			wg.Done()
		}(item, event)
	}
	wg.Wait()
	return nil
}

// AppService
// event true: 表示start, false: 表示stop
func (Self *service) AppService(event bool, exclude ...string) error {
	if exclude == nil {
		exclude = []string{}
	}
	list, err := os.ReadDir(appDir)
	if err != nil {
		return err
	}
	appMap := make(map[string]serviceSlice)
	for _, item := range list {
		info := apputils.CheckAppState(item.Name())
		if info == nil {
			continue
		}
		appMap[info.ID] = serviceSlice{
			list: info.Depend,
			srv:  Self,
		}
	}
	for id, child := range appMap {
		depends := make([]string, 0)
		if len(child.list) > 0 {
			for _, cid := range child.list {
				if dd, ok := appMap[cid]; ok {
					depends = append(depends, dd.list...)
					delete(appMap, cid)
				}
				depends = append(depends, cid)
			}
		}
		depends = append(depends, id)
		appMap[id] = serviceSlice{
			list: utils.RemoveDup(depends),
			srv:  Self,
		}
	}
	wg := sync.WaitGroup{}
	for _, item := range appMap {
		wg.Add(1)
		go func(chs serviceSlice, event bool) {
			chs.sortService(event, exclude)
			wg.Done()
		}(item, event)
	}
	wg.Wait()
	return nil
}

// 按要求的顺序启动或停止app service
func (s serviceSlice) sortService(event bool, exclude []string) {
	if !event {
		for i, j := 0, len(s.list)-1; i < j; i, j = i+1, j-1 {
			s.list[i], s.list[j] = s.list[j], s.list[i]
		}
	}
	for _, id := range s.list {
		if utils.Contains(exclude, id) > -1 {
			continue
		}
		script := filepath.Join(initrd, id)
		isLink, err := utils.IsSymlink(script)
		if err != nil || !isLink {
			continue
		}
		if event {
			_ = s.srv.safeStart(script)
			fmt.Println(script, "\tstart", "\tdone")
		} else {
			_ = s.srv.safeStop(script)
			fmt.Println(script, "\tstop", "\tdone")
		}
	}
}
